package com.zeyu.framework.tools.report.convert;

import com.google.common.collect.Maps;
import com.zeyu.framework.core.common.mapper.JsonMapper;
import com.zeyu.framework.tools.report.charts.ExtendChartData;
import com.zeyu.framework.tools.report.convert.pool.BlockingQueuePool;
import com.zeyu.framework.tools.report.convert.pool.PoolException;
import com.zeyu.framework.tools.report.convert.pool.ServerObjectFactory;
import com.zeyu.framework.tools.report.convert.server.Server;
import com.zeyu.framework.utils.NetUtils;
import com.zeyu.framework.utils.SpringContextHolder;
import com.zeyu.framework.utils.StringUtils;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

import java.io.File;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * 网页图片转换工具
 * Created by zeyuphoenix on 16/9/3.
 */
public class Converter {

    // ================================================================
    // Constants
    // ================================================================

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(Converter.class);

    /** path const */
    public static final String CONVERT_CONFIG = "app-convert.properties";
    public static final String SCRIPT_PATH = "script";

    // ================================================================
    // Fields
    // ================================================================

    // 转换服务队列
    private BlockingQueuePool<Server> serverPool;

    // ================================================================
    // Constructors
    // ================================================================

    /**
     * 不可实例化
     */
    private Converter() {

        // path
        Resource resource = new ClassPathResource("convert" + File.separator + CONVERT_CONFIG);
        Properties properties = new Properties();
        URL path = null;
        try {
            path = resource.getURL();
            properties.load(resource.getInputStream());
        } catch (IOException e) {
            logger.error("convert path is error: ", e);
        }
        logger.info("convert path is {} ", path);

        ServerObjectFactory factory = new ServerObjectFactory();

        // configure factory basic params
        String host = properties.getProperty("host", "127.0.0.1");
        factory.setHost(host.trim());
        String port = properties.getProperty("port", "7777");
        factory.setBasePort(Integer.valueOf(port.trim()));
        String exec = properties.getProperty("exec", "phantomjs");
        Environment environment = SpringContextHolder.getApplicationContext().getEnvironment();
        exec = environment.getProperty("framework.convert.phantomjs") + exec.trim();
        factory.setExec(exec);
        String script = properties.getProperty("script", "echarts-convert.js");
        if (path != null) {
            script = FilenameUtils.getFullPath(path.getPath()) + SCRIPT_PATH + File.separator + script.trim();
        } else {
            script = "convert" + File.separator + SCRIPT_PATH + File.separator + script.trim();
        }
        factory.setScript(script);
        String readTimeout = properties.getProperty("readTimeout", "6000");
        factory.setReadTimeout(Integer.valueOf(readTimeout.trim()));
        String connectTimeout = properties.getProperty("connectTimeout", "500");
        factory.setConnectTimeout(Integer.valueOf(connectTimeout.trim()));
        String maxTimeout = properties.getProperty("maxTimeout", "6500");
        factory.setMaxTimeout(Integer.valueOf(maxTimeout.trim()));

        boolean available = NetUtils.isPortAvailable(factory.getBasePort());
        if (!available) {
            logger.error("port {} is usage, please check phantomjs service is started or not, if other service " +
                    "use this port, please change port in app-convert.properties.", factory.getBasePort());
            logger.error("phantomjs not start, check your system process, use `pkill phantomjs` kill all, and restart server");
            System.exit(1);
            return;
        }

        try {
            // pool's params
            int poolSize = Integer.valueOf(properties.getProperty("poolSize",
                    "10"));
            int maxWait = Integer.valueOf(properties.getProperty("maxWait",
                    "500"));
            int retentionTime = Integer.valueOf(properties.getProperty(
                    "retentionTime", "30000"));
            this.serverPool = new BlockingQueuePool<>(factory, poolSize,
                    maxWait, retentionTime);
            // prevent not initialize
            serverPool.poolCleaner();
        } catch (Exception e) {
            logger.error("block queue create error: ", e);
        }

        // @Scheduled(initialDelay = 30000, fixedRate = 60000)
        final ScheduledExecutorService executorService = Executors
                .newSingleThreadScheduledExecutor();
        executorService.scheduleAtFixedRate(() -> {
            try {
                serverPool.poolCleaner();
            } catch (Exception e) {
                logger.error("server pool cleaner error: ", e);
            }
        }, 30000, 60000, TimeUnit.MILLISECONDS);

        //shutdown will close
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                executorService.shutdownNow();
            }
        });
    }

    // ================================================================
    // Methods from/for super Interfaces or SuperClass
    // ================================================================

    // ================================================================
    // Public or Protected Methods
    // ================================================================

    /**
     * SVGConverter 对象取得
     *
     * @return SVGConverter
     */
    public static Converter getInstance() {

        return SingletonHolder.INSTANCE;
    }

    /**
     * 转换图
     */
    public String convert(ExtendChartData extendChartData)
            throws ConverterException, PoolException, NoSuchElementException,
            TimeoutException {
        return this.convert(null, null, null, null, extendChartData);
    }

    /**
     * 转换图
     *
     * @param width
     *            : "1000px" 或者 "100%"格式
     * @param height
     *            : "400px" 格式
     */
    public String convert(String width, String height,
                          ExtendChartData extendChartData) throws ConverterException,
            PoolException, NoSuchElementException, TimeoutException {
        return this.convert(width, height, null, null, extendChartData);
    }

    /**
     * 转换图
     *
     * @param width
     *            : "1000px" 格式
     * @param height
     *            : "400px" 格式
     * @param border
     *            : "1px solid #ccc" 格式
     * @param padding
     *            : "25px" 格式
     */
    public String convert(String width, String height, String border,
                          String padding, ExtendChartData extendChartData)
            throws ConverterException, PoolException, NoSuchElementException,
            TimeoutException {

        Map<String, Object> params = Maps.newHashMap();

        if (extendChartData != null) {
            params.put("chartoptions", extendChartData);
        } else {
            throw new ConverterException("ExtendChartData must has value!");
        }

        if (StringUtils.isNotBlank(width)) {
            params.put("width", width);
        } else {
            params.put("width", "1000px");
        }

        if (StringUtils.isNotBlank(width)) {
            params.put("height", height);
        } else {
            params.put("height", "400px");
        }

        if (StringUtils.isNotBlank(border)) {
            params.put("border", border);
        } else {
            params.put("border", "1px solid #ccc");
        }
        if (StringUtils.isNotBlank(padding)) {
            params.put("padding", padding);
        } else {
            params.put("padding", "25px");
        }

        // parameters to JSON
        String json = JsonMapper.getInstance().toJson(params);

        // send to phantomJs
        String output;
        output = requestServer(json);

        // check first for errors
        if (output.length() > 5
                && output.substring(0, 5).equalsIgnoreCase("error")) {
            throw new ConverterException("recveived error from phantomjs:"
                    + output);
        }

        return output;
    }

    /**
     * 请求图表
     */
    public String requestServer(String params) throws ConverterException,
            TimeoutException, NoSuchElementException, PoolException {
        Server server = null;

        try {
            server = serverPool.borrowObject();

            return server.request("body='" + params + "'");
        } catch (SocketTimeoutException | TimeoutException ste) {
            logger.error("timeout: ", ste);
            throw new TimeoutException(ste.getMessage());
        } catch (PoolException nse) {
            logger.error("POOL EXHAUSTED!!", nse);
            throw new PoolException(nse.getMessage());
        } catch (Exception e) {
            logger.error("", e);
            throw new ConverterException("Error converting " + e.getMessage());
        } finally {
            try {
                serverPool.returnObject(server, true);
            } catch (Exception e) {
                logger.error("Exception while returning server to pool: "
                        + e.getMessage());
            }
        }
    }

    // ================================================================
    // Getter & Setter
    // ================================================================

    // ================================================================
    // Private Methods
    // ================================================================

    // ================================================================
    // Inner or Anonymous Class
    // ================================================================

    /**
     * 静态内部类 优点：加载时不会初始化静态变量INSTANCE，因为没有主动使用，达到Lazy
     *
     * @author zeyuphoenix
     */
    private static class SingletonHolder {
        private final static Converter INSTANCE = new Converter();
    }

    // ================================================================
    // Test Methods
    // ================================================================

}
